package com.neo.tiny.oauth.service.impl;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.neo.tiny.admin.api.user.UserAuthApi;
import com.neo.tiny.admin.vo.user.AuthUserDetails;
import com.neo.tiny.common.constant.CacheConstants;
import com.neo.tiny.common.constant.ErrorCodeConstants;
import com.neo.tiny.common.enums.UserTypeEnum;
import com.neo.tiny.common.exception.WebApiException;
import com.neo.tiny.oauth.entity.SysOauth2ClientDO;
import com.neo.tiny.oauth.entity.SysOauth2CodeDO;
import com.neo.tiny.oauth.service.Oauth2GrantService;
import com.neo.tiny.oauth.service.Oauth2TokenService;
import com.neo.tiny.oauth.service.SysOauth2CodeService;
import com.neo.tiny.secrity.util.SecurityUtils;
import com.neo.tiny.service.RedisService;
import com.neo.tiny.token.store.OAuth2AccessToken;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;

/**
 * @author yqz
 * @Description oAuth 授权
 * @CreateDate 2022/11/11 10:24
 */
@Slf4j
@Service
@AllArgsConstructor
public class Oauth2GrantServiceImpl implements Oauth2GrantService {


    private final SysOauth2CodeService oauth2CodeService;

    private final Oauth2TokenService oauth2TokenService;

    private final UserAuthApi userAuthApi;

    private final RedisService redisService;


    @Override
    public OAuth2AccessToken grantImplicit(Long userId, String userName, Integer userType, String clientId, List<String> scopes) {
        return oauth2TokenService.createAccessToken(userId, userName, userType, clientId, scopes);
    }

    @Override
    public String grantAuthorizationCodeForCode(Long userId, String userName, Integer userType, String clientId, List<String> scopes, String redirectUri, String state) {
        SysOauth2CodeDO authorizationCode = oauth2CodeService.createAuthorizationCode(userId, userName, userType, clientId, scopes, redirectUri, state);

        return authorizationCode.getCode();
    }

    @Override
    public OAuth2AccessToken grantAuthorizationCodeForAccessToken(String clientId, String code, String redirectUri, String state) {
        SysOauth2CodeDO codeDO = oauth2CodeService.consumeAuthorizationCode(code);
        // 防御性编程
        Assert.notNull(codeDO, "授权码不能为空");
        // 校验 clientId 是否匹配
        if (!StrUtil.equals(clientId, codeDO.getClientId())) {
            throw new WebApiException(ErrorCodeConstants.OAUTH2_GRANT_CLIENT_ID_MISMATCH);
        }
        // 校验 获取code时传入的redirectUri和当前传入的redirectUri 是否匹配
        if (!StrUtil.equals(redirectUri, codeDO.getRedirectUri())) {
            throw new WebApiException((ErrorCodeConstants.OAUTH2_GRANT_REDIRECT_URI_MISMATCH));
        }
        // 校验 state 是否匹配，数据库 state 为 null 时，会设置为 "" 空串
        state = StrUtil.nullToDefault(state, "");
        if (!StrUtil.equals(state, codeDO.getState())) {
            throw new WebApiException((ErrorCodeConstants.OAUTH2_GRANT_STATE_MISMATCH));
        }

        // 创建访问令牌
        return oauth2TokenService.createAccessToken(codeDO.getUserId(), codeDO.getUserName(), codeDO.getUserType(), codeDO.getClientId(), codeDO.getScopes());
    }

    @Override
    public OAuth2AccessToken grantPassword(String username, String password, String clientId, List<String> scopes) {

        // 使用账密进行登录
        AuthUserDetails userDetails = userAuthApi.authenticate(username, password);

        // 防御
        Assert.notNull(userDetails, "无该用户");

        // 创建令牌
        return oauth2TokenService.createAccessToken(userDetails.getUserId(), userDetails.getUsername(), UserTypeEnum.ADMIN.getValue(), clientId, scopes);
    }

    @Override
    public OAuth2AccessToken grantRefreshToken(String refreshToken, String clientId) {
        return oauth2TokenService.refreshAccessToken(refreshToken, clientId);
    }

    @Override
    public OAuth2AccessToken grantClientCredentials(SysOauth2ClientDO client, String clientId, String clientSecret, List<String> scopes) {
        if (clientSecret == null || "".equals(clientSecret)) {
            log.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException("Bad credentials");
        }

        if (!clientSecret.equals(client.getClientSecret())) {
            log.debug("Authentication failed: password does not match stored value");
            throw new BadCredentialsException("Bad credentials");
        }
        return oauth2TokenService.createAccessToken(client.getId(), client.getClientId(),
                UserTypeEnum.CLIENT.getValue(), clientId, scopes);

        // TODO 项目中使用 OAuth2 解决的是三方应用的授权，内部的 SSO 等问题，所以暂时不考虑 client_credentials 这个场景
        //throw new UnsupportedOperationException("暂时不支持 client_credentials 授权模式");
    }

    @Override
    public boolean removeToken(String clientId, String accessToken, Integer removeScope) {

        // 先校验客户端
        OAuth2AccessToken auth2AccessToken = oauth2TokenService.getAccessToken(accessToken);

        if (Objects.isNull(auth2AccessToken) || !Objects.equals(auth2AccessToken.getClientId(), clientId)) {
            return false;
        }
        // 退出登录后，清除用户信息缓存
        String username = SecurityUtils.getUserName();
        String key = CacheConstants.USER_DETAILS + StrUtil.COLON + username;
        redisService.del(key);
        // 删除
        return oauth2TokenService.removeAccessToken(accessToken, removeScope) != null;
    }
}
